#!/usr/bin/env python
# encoding: utf-8
'''
Created on Jun 16, 2011

@author: apple
'''
import types,cPickle,xapian
from mmseg.search import seg_txt_2_dict
try:
    from config import SEARCH_DB_PATH
except Exception, e:
    import sys
    if sys.platform.startswith('win'):
        SEARCH_DB_PATH = 'E:\\xapiandb'
    elif sys.platform.startswith('darwin'):
        SEARCH_DB_PATH = '/Users/apple/Project/xapiandb'
    else:
        SEARCH_DB_PATH = '/var/xapiandata'

all = ['SEARCH_DB_PATH' , '__isenglish__' , 'SearchConnection' , 'IndexConnection']

AND            = xapian.Query.OP_AND
OR             = xapian.Query.OP_OR
AND_NOT        = xapian.Query.OP_AND_NOT
XOR            = xapian.Query.OP_XOR
OP_AND_MAYBE   = xapian.Query.OP_AND_MAYBE
OP_FILTER      = xapian.Query.OP_FILTER
OP_NEAR	       = xapian.Query.OP_NEAR
OP_PHRASE	   = xapian.Query.OP_PHRASE
OP_VALUE_RANGE = xapian.Query.OP_VALUE_RANGE
OP_SCALE_WEIGHT= xapian.Query.OP_SCALE_WEIGHT
OP_ELITE_SET   = xapian.Query.OP_ELITE_SET
OP_VALUE_GE	   = xapian.Query.OP_VALUE_GE
OP_VALUE_LE    = xapian.Query.OP_VALUE_LE

Query  = xapian.Query

"""
IndexConnection + WriteConnection
"""

def index(indexfield,document,docid=None):
    """
    Write and index document
    if has a docid then update the document and reindex
    """
    if type(indexfield) != types.ListType or type(document) != types.DictType:
        return False
    conn = WriteConnection(docid=docid,**document)
    return conn.commit(*indexfield)
    
def search(s,unserialize=True,skip=0,limit=10):
    """
    Simple search interface
    """
#    raise TypeError,s+repr(type(s))
    try:
        s = s.encode('utf-8')
    except:
        pass
    return SearchConnection().search(s,unserialize=unserialize,skip=skip,limit=limit)
    
def update(docid,opera,indexfield,doc):
    return WriteConnection().update(docid,opera,indexfield,**doc)
    
def delete(docid):
    return WriteConnection().delete(docid)
    
def raw_read(docid):
    """
    Read a document by id
    """
    return ReadConnection().read(docid,unserialize=True)
    
def raw_write(doc):
    """
    Write a document 
    but the function don't index the document
    """
    serialize = ( type(doc) != types.StringType )
    return WriteConnection().write(doc,serialize=serialize)
    
def raw_replace(docid,doc):
    """
    Simple update interface
    replace the document
    the function don't reindex .
    """
    serialize = ( type(doc) != types.StringType )
    return WriteConnection().replace(docid,doc,serialize=serialize)

def __isenglish__(s):
    """docstring for __"""
    if not s : return True
    s = s.strip()
    return ord(s[0]) < 127
    
def __is_string__(s):
    """docstring for __is_string__"""
    return type(s) == types.StringType or type(s) == types.UnicodeType

def __format_index__(fields,obj):
    """docstring for __format_index__"""
    for i in fields:
        field = obj.get(i,None)
        if not __is_string__(field):
             obj[i] = __flat__(field)
    return obj

def __flat__(field):
    result = []
    if type(field) == types.ListType:
        for i in field:
            if not __is_string__(i):
                result.extend(__flat__(i))
            else:
                result.append(i)
    elif type(field) == types.DictType:
        for k,v in field.items():
            if not __is_string__(v):
                result.extend(__flat__(v))
            else:
                result.append(v)
    elif __is_string__(field):
        result.append(field)
    return ' '.join(result)
        
class Connection(object):
    def __init__(self,path=None):
        self.lasterror = ''
        self.path = path or SEARCH_DB_PATH
        self.prefix = 'Z%s'
        
    @property
    def error(self):
        return self.lasterror
        
class SearchConnection(Connection):
    def __init__(self,path=None,**kv):
        Connection.__init__(self,path)
        try:
            self.db = xapian.Database(self.path)
        except:
            self.db = None
            self.lasterror = 'Database error.'
            raise xapian.DatabaseError,self.lasterror
    
    def __del__(self):
        try:
            self.db.close()
        except:
            pass
            
    def search(self,keywords,**kv):
        """
        keywords type is list or string
        if the's string auto seg
        kv skip int limit,unserialize boolean
        """
        if not self.db:
            return []
        skip    = kv.pop('skip',0)
        limit   = kv.pop('limit',10)
        enquire = xapian.Enquire(self.db)
        query   = self.__buildquery__(keywords,**kv)
#        raise TypeError,repr(str(query))
#        raise TypeError,repr(query.get_length())
        try:
            enquire.set_query(query)
            result  = enquire.get_mset(skip,limit)
            unserialize = kv.pop('unserialize',None)
            decode = cPickle.loads if unserialize else lambda x:x
            return [{'detail':r,'data':decode(r.document.get_data())} for r in result]
        except Exception,e:
            raise TypeError,'query error.%s'%e
            return []
            
    def query(self,*querys,**kv):
        if not self.db:
            self.lasterror = 'Database error.'
            return []
        for q in querys:
            if not isinstance(Query,q):
                self.lasterror = 'Query type error.'
                return []
        skip    = kv.pop('skip',0)
        limit   = kv.pop('limit',10)
        opera = kv.pop('opera',OR)
        unserialize = kv.pop('unserialize',None)
        decode = cPickle.loads if unserialize else lambda x:x
        enquire = xapian.Enquire(self.db)
        query = Query(opera,querys)
        return [{'detail':r,'data':decode(r.document.get_data())} for r in enquire.get_mset(skip,limit)]
        
    def __buildquery__(self,keywords,**kv):
        if type(keywords) == types.UnicodeType:
            keywords = keywords.encode('utf-8')
        if type(keywords) == types.StringType:
            return self.__buildenglish__(keywords,**kv) if __isenglish__(keywords) else self.__buildchinese__(keywords,**kv)
        elif type(keywords) == types.ListType:
            opera = kv.pop('opera',OR)
            return xapian.Query(self.prefix%opera,[xapian.Query(i) for i in keywords])
        elif type(keywords) == types.DictType:
            opera = kv.pop('opera',OR)
            return xapian.Query(opera,[xapian.Query(self.prefix%word,value) for word,value, in keywords.items()])
        else:
            self.lasterror = 'keywords type error.'
            raise TypeError,'keywords must is string or list or dict of keyword:value'
        
    def __buildenglish__(self,keywords,**kv):
        strategy = kv.pop('strategy',xapian.QueryParser.STEM_SOME)
        qp = xapian.QueryParser()
        qp.set_database(self.db)
        stemmer = xapian.Stem('english')
        qp.set_stemmer(stemmer)
        qp.set_stemming_strategy(strategy)
        qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
        query = qp.parse_query(keywords)
        return query
        
    def __buildchinese__(self,keywords,**kv):
        opera = kv.pop('opera',OR)
        #add a prefix
        querys = [xapian.Query(self.prefix%word,value) for word,value in seg_txt_2_dict(keywords).iteritems()]
        return querys[0] if len(querys) == 1 else xapian.Query(opera,querys)
        
        
class ReadConnection(Connection):
    """
    Read document by id
    """
    def __init__(self, path=None):
        Connection.__init__(self,path)
        try:
            self.db = xapian.Database(self.path)
        except:
            self.db = None
            self.lasterror = 'Database error.'
            raise xapian.DatabaseError,self.lasterror
        
    def read(self,docid,**kv):
        if not self.db:
            self.lasterror = 'Database error.'
            return {}
        decode = cPickle.loads if kv.get('unserialize',False) else lambda x:x
        try:
            doc = self.db.get_document(docid).get_data()
            return decode(doc)
        except Exception, e:
            self.lasterror = 'Read error.%s'%e
            return {}
            
    def get_raw(self,docid):
        if not self.db:
            self.lasterror = 'Database error.'
            return None
        try:
            return self.db.get_document(docid)
        except Exception, e:
            self.lasterror = 'Read error.%s'%e
            return None
            
class WriteConnection(Connection):
    """
    Write document
    """
    def __init__(self, path=None,docid=None,**document):
        Connection.__init__(self,path)
        if docid:
            self.doc = ReadConnection().read(docid)
        else:
            self.doc = {}
        if type(self.doc) != types.DictType:
            try:
                doc = cPickle.loads(doc)
            except Exception, e:
                self.lasterror = 'Unserialize document error.'
                self.docid,self.doc = None,{}
        self.docid = docid
        self.doc.update(document)
        try:
            self.db = xapian.WritableDatabase(self.path,xapian.DB_CREATE_OR_OPEN)
        except Exception,e:
            self.db = None
            self.lasterror = 'Database error.%s'%e
            raise xapian.DatabaseError,self.lasterror
            
    def __make_index__(self,*fields):
        if not self.doc:
            self.lasterror = 'Database error.'
            return False
        self.indexfield = []
        for i in fields:
            field = self.doc.get(i,None)
            if __is_string__(field):
                self.indexfield.append(field.encode('utf-8'))
            else:
                self.indexfield.append(__flat__(field).encode('utf-8'))
        
    def update(self,docid,opera,indexfield,**doc):
        """
        """
        if not self.db:
            self.lasterror = 'Database error.'
            return False
        if not docid:
            self.lasterror = 'Empty document id.'
            return False
        if not doc:
            self.lasterror = 'Empty document.'
            return False
        for k,v in doc.items():
            self.__update(docid,opera,indexfield,{k:v})
        try:
            self.db.flush()
            return True
        except Exception,e:
            self.lasterror = 'Flush Database error.%s'%e
            return False
    
    def __update(self,docid,opera,indexfield,doc):
        document = ReadConnection().get_raw(docid)
        old = cPickle.loads(document.get_data())
        if opera == '$set':
#            self.indexfield = [doc.get(i,'').encode('utf-8') for i in indexfield]
            self.doc = doc
            self.__make_index__(*indexfield)
            old.update(doc)
        elif opera == '$push':
            olditem = old.get(doc.keys()[0],[])
#            self.indexfield = [doc.values()[0].get(i,'').encode('utf-8') for i in indexfield]
            self.doc = doc.values()[0]
            self.__make_index__(*indexfield)
            olditem.append(doc.values()[0])
            old[doc.keys()[0]] = olditem
        document.set_data(cPickle.dumps(old))
        indexer = xapian.TermGenerator()
        indexer.set_stemmer(xapian.Stem('english'))
        indexer.index_text(' '.join(self.indexfield)) 
        for i in indexer.get_document().termlist():
            document.add_term(i.term)
        self.db.replace_document(docid,document)
        return self.docid
        
    def commit(self,*indexfield):
        if not self.doc:
            self.lasterror = 'Empty document.'
            return False
        self.__make_index__(*indexfield)
        return self.__insert__(self.indexfield)
        
    def __insert__(self,indexfield):
        if type(indexfield) != types.ListType:
            self.lasterror = 'Indexfield type error.'
            return False
        if not len(indexfield):
            self.lasterror = 'Empty indexfield.'
            return False
        return self.__insertenglish__(indexfield) if __isenglish__(indexfield[0]) else self.__insertchinese__(indexfield)
        
    def __insertenglish__(self,indexfield):
        indexer = xapian.TermGenerator()
        stemmer = xapian.Stem("english")
        indexer.set_stemmer(stemmer)
        document = xapian.Document()
        document.set_data(cPickle.dumps(self.doc))
        indexer.set_document(document)
        indexer.index_text(' '.join(indexfield))
        try:
            if self.docid:
                self.db.replace_document(self.docid,document)
                self.db.flush()
                return self.docid
            else:
                docid = self.db.add_document(document)
                self.db.flush()
                return docid
        except Exception,e:
            print e
            self.lasterror = 'Flush db error.'
            return False
            
    def __insertchinese__(self, indexfield):
        document = xapian.Document()
        document.set_data(cPickle.dumps(self.doc))
        words = ' '.join(indexfield)
        for word,value in seg_txt_2_dict(words).iteritems():
            document.add_term(self.prefix%word,value)
        try:
            if self.docid:
                self.db.replace_document(self.docid,document)
                self.db.flush()
                return self.docid
            else:
                docid = self.db.add_document(document)
                self.db.flush()
                return docid
        except Exception,e:
            self.lasterror = 'Flush db error.%s'%e
            return False
        
        
    def index(self,document,indexfield=[]):
        if indexfield and type(indexfield) != types.ListType:
            self.lasterror = 'Indexfield type error.'
            return False
        self.doc = document if type(document) == types.StringType else cPickle.dumps(document)
        return self.commit(*indexfield)
            
    def write(self,doc,**kv):
        if not self.db:
            self.lasterror = 'Database error.'
            return False
        encode = cPickle.dumps if kv.get('serialize',False) else lambda x:x
        try:
            docid = self.db.add_document(encode(doc))
            self.db.flush()
#            Process(target=self.db.flush,args=()).start()
            return docid
        except Exception, e:
            self.lasterror = 'Write error.%s'%e
            return False
    
    def replace(self,docid,doc,**kv):
        if not self.db:
            self.lasterror = 'Database error.'
            return False
        encode = cPickle.dumps if kv.get('serialize',False) else lambda x:x
        try:
            document = xapian.Document()
            document.set_data(encode(doc))
            self.db.replace_document(docid,document)
            self.db.flush()
#            Process(target=self.db.flush,args=()).start()
            return docid
        except Exception, e:
            self.lasterror = 'Write error.%s'%e
            return False
            
    def delete(self,docid):
        """
        delete document
        """
        if not self.db:
            self.lasterror = 'Database error.'
            return False
        try:
            return self.db.delete_document(docid)
        except Exception, e:
            self.lasterror = 'Delect error.%s'%e
            return False
            
    def __len__(self):
        if not self.db:
            self.lasterror = 'Database error.'
            return False
        try:
            return self.db.get_doccount()
        except Exception, e:
            self.lasterror = 'Count error.%s'%e
            return False
            
if __name__ == '__main__':
    print delete(72)
#    wc = WriteConnection()
#    print wc.update(9,'$push',['answer','uid'],answers={'answer':'pointing to start of positionlist','uid':'ida'})
#    print wc.error
#      print update(9,'$push',['answer','uid'],{'answers':{'answer':'pointing to start of positionlist','uid':'ida'}})
#    print raw_read(9)
#    print raw_replace(73,{'author':'joson','title':'footbar golang','summary':'italy usa tokyo11','extra':'alan'})
   # print search('联系')
#    print raw_read(73)
#    print index(['author','title','summary'],{'author':'青少年','title':'上赛季利物浦获得了联赛的第六名，新赛季他们的目标应该是进入前四，锁定一个欧冠联赛的席位，不过杰拉德表示，红军在传统上是冠军级别的队伍，他们的目标会更高。','summary':'italy usa tokyo11','extra':'alan'})
    

#    print SearchConnection().search('精英赛',unserialize=True,opera=OR)